import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;
import java.io.IOException;
public class FeedForwardNN implements Serializable{
	protected FFNeuron[] neurons;
	protected FFSynapse[] synapses;
	protected Layer[] layers;

	class Layer implements Serializable{
		FFNeuron[] neurons;
		public Layer(int numLayers) {
			int nCount = 0;
			for (int i = 0; i < FeedForwardNN.this.neurons.length; i++)
				if (FeedForwardNN.this.neurons[i].layer == numLayers) nCount++;
			this.neurons = new FFNeuron[nCount];
			nCount = 0; 
			for (int i = 0; i < FeedForwardNN.this.neurons.length; i++) 
				if (FeedForwardNN.this.neurons[i].layer == numLayers)
					this.neurons[nCount++] = FeedForwardNN.this.neurons[i];
		}
	}
	
	public FeedForwardNN (int[] numNeurons, double[] learningRateCoefficient, TransferFunction[] transFunc, double[] momentumRate, double[] axonFlatness) {
		int nCount = 0;
		for (int i = 0; i < numNeurons.length; i++) nCount += numNeurons[i];
		neurons = new FFNeuron[nCount];
		nCount = 0;
		for ( int i = 0; i < numNeurons.length-1; i++ ) nCount += numNeurons[i] * numNeurons[i+1];
		synapses = new FFSynapse[nCount];
		nCount = 0;
		for ( int i = 0; i < numNeurons[0]; i++ ) neurons[nCount++] = new FFNeuron();
		for ( int i = 1; i < numNeurons.length; i++ ) 
			for ( int j = 0; j < numNeurons[i]; j++ ) 
				neurons[nCount++] = new FFNeuron(i, axonFlatness[i], transFunc[i], momentumRate[i], learningRateCoefficient[i]);
		layers = new Layer[numNeurons.length];
		for (int i = 0; i < numNeurons.length; i++) {layers[i] = new Layer(i);}
		nCount = 0;
		for ( int i = 0; i < numNeurons.length-1; i++) 
			for ( int j = 0; j < layers[i].neurons.length; j++ ) 
				for ( int k = 0; k < layers[i+1].neurons.length; k++ ) 
					synapses[nCount++] = new FFSynapse(layers[i].neurons[j], layers[i+1].neurons[k]);
		linkingNeurons();
	}	

	private void linkingNeurons () {
		FFNeuron[] tempNeuronsIn;
		FFNeuron[] tempNeuronsOut;
		FFSynapse[] tempSynapsesIn;
		FFSynapse[] tempSynapsesOut;
		
		int nCountIn; 
		int nCountOut;
		for (int i = 0; i < neurons.length; i++) {
			tempNeuronsIn = null;
			tempNeuronsOut = null;
			nCountIn = 0; nCountOut = 0;
			for (int j = 0; j < synapses.length; j++) {
				if (synapses[j].sourceNeuron == neurons[i]) {nCountOut++;}
				if (synapses[j].targetNeuron == neurons[i]) {nCountIn++;}
			}
			tempNeuronsIn = new FFNeuron[nCountIn];
			tempSynapsesIn = new FFSynapse[nCountIn];
			tempNeuronsOut = new FFNeuron[nCountOut];
			tempSynapsesOut = new FFSynapse[nCountOut];
			nCountIn = 0; nCountOut = 0;
			for (int j = 0; j < synapses.length; j++) {
				if (synapses[j].sourceNeuron == neurons[i]) {
					tempNeuronsOut[nCountOut] = synapses[j].targetNeuron;
					tempSynapsesOut[nCountOut++] = synapses[j];
				}
				if (synapses[j].targetNeuron == neurons[i]) {
					tempNeuronsIn[nCountIn] = synapses[j].sourceNeuron;
					tempSynapsesIn[nCountIn++] = synapses[j];
				}
			}
			neurons[i].setLinkings(tempNeuronsIn, tempNeuronsOut, tempSynapsesIn, tempSynapsesOut);
		}
	}

	public void feedForward (double[] inputs) {
		for (int i = 0; i < layers[0].neurons.length; i++) {
			layers[0].neurons[i].output = inputs[i];
		}
		for (int i = 1; i < layers.length; i++) {
			for (int j = 0; j < layers[i].neurons.length; j++) {
				layers[i].neurons[j].updateOutput();
			}
		}
	}
	
	public double[] output (double[] inputs) {
		feedForward(inputs);
		double[] tempoutputs = new double[layers[layers.length-1].neurons.length];
		for(int i = 0; i < layers[layers.length-1].neurons.length; i++) {
			tempoutputs[i] = layers[layers.length-1].neurons[i].output;
		}
		return tempoutputs;
	}

	public void train(double rate, Pattern pattern, LearningMode learningMode){
		feedForward(pattern.input);
		for (int j = 0; j < layers[layers.length-1].neurons.length; j++) 
			layers[layers.length-1].neurons[j].trainOutputLayer(rate, pattern.target[j],learningMode);
		for (int i = layers.length-2; i > 0; i--) 
			for (int j = 0; j < layers[i].neurons.length; j++) 
				layers[i].neurons[j].trainHiddenLayer(rate,learningMode);
	}
	public void incrementalTrain(double rate,Pattern[] patterns) {
		int patternsNotTrained = patterns.length;
		int patternTrain;
		int patternIndex = -1;
		int nCount;
		for (int i = 0; i < patterns.length; i++) patterns[i].selected = false;
		for (int i = 0; i < patterns.length; i++) {
			patternTrain = RandGen.nextInt(patternsNotTrained);
			nCount = 0;
			for (int j = 0; j < patterns.length; j++)
				if (!patterns[j].selected) {
					if (nCount != patternTrain) nCount++;
					else if (nCount == patternTrain) {
						patternIndex = j;
						break;
					}
				}
			train(rate, patterns[patternIndex],LearningMode.INCREMENTAL);
			patterns[patternIndex].selected = true;
			patternsNotTrained--;
		}
		for (int i = 0; i < patterns.length; i++) {
			patterns[i].selected = false;
		}
	}
	public void batchTrain(double rate,Pattern[] patterns, int numPatterns) {
		int patternsNotTrained = patterns.length;
		if (numPatterns > patterns.length) {numPatterns = patterns.length;}
		if (numPatterns < 1) {numPatterns = 1;}
		int patternTrain;
		int numSelectedPatterns;
		Pattern[] selectedPatterns;
		int patternIndex = -1;
		int[] patternsTrainIndex = new int[numPatterns];
		int nCount;	
		for (int i = 0; i < patterns.length; i++) {
			patterns[i].selected = false;
		}
		while ( patternsNotTrained > 0 ) {
			numSelectedPatterns = 0;
			while ( numSelectedPatterns < numPatterns && patternsNotTrained > 0 ) {
				patternTrain = RandGen.nextInt(patternsNotTrained);
				patternsNotTrained--;
				nCount = 0;
				for (int i = 0; i < patterns.length; i++) {
					if (!patterns[i].selected) {
						if (nCount != patternTrain) {
							nCount++;
						}
						else if (nCount == patternTrain) {
							patternIndex = i;
							break;
						}
					}
				}
				numSelectedPatterns++;
				patternsTrainIndex[numSelectedPatterns-1] = patternIndex;
				patterns[patternIndex].selected = true;
			}
			selectedPatterns = null;
			selectedPatterns = new Pattern[numSelectedPatterns];
			for (int i = 0; i < numSelectedPatterns; i++) {
				selectedPatterns[i] = patterns[patternsTrainIndex[i]];
			}
			batchTrain(rate, selectedPatterns);
		}
		for (int i = 0; i < patterns.length; i++) {
			patterns[i].selected = false;
		}
	}
	public void batchTrain(double rate,Pattern[] patterns) {
		for (int i = 0; i < patterns.length; i++) {
			train(rate, patterns[i],LearningMode.BATCH);
		}
		for  ( int i = 0; i < neurons.length; i++ ) {
			neurons[i].batchUpdateWeights(patterns.length);
		}
	}

	public double errorRatio (DataSet patternset) {
		int NumOutputNeurons = layers[layers.length-1].neurons.length;
		double[] absErrors = new double[NumOutputNeurons];
		for ( int i = 0; i < NumOutputNeurons; i++ ) { absErrors[i] = 0; }
		double errorRatio = 0;
		double[] temp_output = new double[NumOutputNeurons];
		for (int j = 0; j < patternset.crossValPatterns.length; j++) {
			temp_output = output(patternset.crossValPatterns[j].input);
			for (int i = 0; i < NumOutputNeurons; i++) {
				absErrors[i] += Math.abs( temp_output[i] - patternset.crossValPatterns[j].target[i] );
			}
		}
		for (int i = 0; i < NumOutputNeurons; i++) {
			absErrors[i] /= patternset.crossValPatterns.length;
			errorRatio += ( absErrors[i] / patternset.crossvaldeviations[i] );
		}
		errorRatio /= NumOutputNeurons;
		return errorRatio;
	}

	public double TestErrorRatio (DataSet patternset) {
		int NumOutputNeurons = layers[layers.length-1].neurons.length;
		double[] absErrors = new double[NumOutputNeurons];
		for ( int i = 0; i < NumOutputNeurons; i++ ) { absErrors[i] = 0; }
		double errorRatio = 0;
		double[] temp_output = new double[NumOutputNeurons];
		for (int j = 0; j < patternset.testPatterns.length; j++) {
			temp_output = output(patternset.testPatterns[j].input);
			for (int i = 0; i < NumOutputNeurons; i++) {
				absErrors[i] += Math.abs( temp_output[i] - patternset.testPatterns[j].target[i] );
			}
		}
		for (int i = 0; i < NumOutputNeurons; i++) {
			absErrors[i] /= patternset.testPatterns.length;
			errorRatio += ( absErrors[i] / patternset.testdeviations[i] );
		}
		errorRatio /= NumOutputNeurons;
		return errorRatio;
	}
}